
import java.util.ArrayList;
import javax.vecmath.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.io.*;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import javax.media.opengl.*;
import javax.media.opengl.glu.GLU;
import javax.swing.JFrame;
import javax.vecmath.Point3f;
import javax.vecmath.Vector3f;

import com.sun.opengl.util.BufferUtil;
import com.sun.opengl.util.FPSAnimator;
import com.sun.opengl.util.GLUT;

import java.util.ArrayList;

/*
 The terrainGenerator generates a random fractal terrain by creating a 2D-array filled with 3D points.
 The y-values of these points are retrieved from an 1D-x*x-vector (yVals) that contains a set of floating point 
 values generated by the algorithm. The algorithm is known as the diamond-square algorithm and this implementation
 is based on a texture map implementation retrieved here: http://www.gameprogrammer.com/fractal.html#selfsim
 */
public class TerrainGenerator {

    int dim;
    float min;
    float max;
    float seed;
    int stride;
    float len;
    int size;
    float[] yVals;
    boolean oddline;
    Vector3f[][] points;
    float tempRand;
    float fallOff = 1.f;
    float sharp;
    int smooth;

    // Dimension: number of quads squared - Sharpness: sets "randomness" of topography - Length: length of mesh in space.
    TerrainGenerator(int dimension, float sharpness, float length) {
        dim = dimension;
        len = length;
        sharp = sharpness;
        smooth = 9;
        min = (float) -sharpness * 0.8f;
        max = (float) sharpness * 1f;
        size = dim;
        size++;
        stride = dim / 2;
        points = new Vector3f[(int) size][(int) size];
        yVals = new float[size * size];
        generateSurface(points);
    }

    // Main function, outputs a 2D matrix of vector3f points.
    void generateSurface(Vector3f[][] points) {
        // The corner points should all be initialized to 0.
        yVals[0] = 0.f;
        yVals[dim] = 0.f;
        yVals[(size) * (size) - dim] = 0.f;
        yVals[(size) * (size) - 1] = 0.f;

        // This while-loop contains the algorithm. It will run until the variable stride is 0.
        // Stride is half the length of one side of the square which is halved until it hits zero.
        while (stride != 0) {
            // This first if-statement is a hack to make the scene more mountaineous by allowing for huge random values in the first pass.
            if (stride == dim / 2) {
                max *= sharp*0.8;
                min *= sharp*0.8;
            }
            else if (stride == dim / 4) {
                max *= sharp*0.6;
                min *= sharp*0.6;
            }
            for (int i = stride; i < dim; i += stride) {
                for (int j = stride; j < dim; j += stride) {
                    tempRand = generateRandom() * fallOff;
                    yVals[(i * size) + j] = tempRand + avgSquare(i, j);
                    j += stride;
                }
                i += stride;
            }
            oddline = false;
            for (int i = 0; i < dim; i += stride) {
                oddline = !oddline;
                for (int j = 0; j < dim; j += stride) {
                    tempRand = generateRandom() * fallOff;
                    if ((oddline)) {
                        j += stride;
                    }
                    yVals[(i * size) + j] = tempRand + avgDiamond(i, j);
                    if (i == 0) {
                        yVals[(dim * size) + j] =
                                yVals[(i * size) + j];
                    }
                    if (j == 0) {
                        yVals[(i * size) + dim] =
                                yVals[(i * size) + j];
                    }
                    if ((!oddline)) {
                        j += stride;
                    }
                }
            }
            stride >>= 1;
            // The fall off is set by "sharpness" parameter.
            // It reduces the effect of the random value at every pass through the loop to create smoother peaks and valleys.
            fallOff *= Math.log(sharp) / 2f;
        }
        for (int i = 0; i < smooth; i++) {
            smoothing();
        }
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                points[i][j] = new Vector3f((float) (len / dim) * i - (len / 2), yVals[i * size + j], (float) (len / dim) * j - (len / 2));
            }
        }

    }
    // Generates a random value within the max-min range.
    float generateRandom() {
        return (float) Math.random() * (max - min) + min;
    }
    /*
     Computes "diamond"-points (*-points below) based on the center point and the corner points (forms 4 diamonds):
     X   *   X
    
     *   X   *
     
     X   *   X
     */
    float avgDiamond(int i, int j) {
        if (i == 0) {
            return ((float) (yVals[(i * size) + j - stride]
                    + yVals[(i * size) + j + stride]
                    + yVals[((dim - stride) * size) + j]
                    + yVals[((i + stride) * size) + j]) * .25f);
        } else if (i == size - 1) {
            return ((float) (yVals[(i * size) + j - stride]
                    + yVals[(i * size) + j + stride]
                    + yVals[((i - stride) * size) + j]
                    + yVals[((0 + stride) * size) + j]) * .25f);
        } else if (j == 0) {
            return ((float) (yVals[((i - stride) * size) + j]
                    + yVals[((i + stride) * size) + j]
                    + yVals[(i * size) + j + stride]
                    + yVals[(i * size) + dim - stride]) * .25f);
        } else if (j == size - 1) {
            return ((float) (yVals[((i - stride) * size) + j]
                    + yVals[((i + stride) * size) + j]
                    + yVals[(i * size) + j - stride]
                    + yVals[(i * size) + 0 + stride]) * .25f);
        } else {
            return ((float) (yVals[((i - stride) * size) + j]
                    + yVals[((i + stride) * size) + j]
                    + yVals[(i * size) + j - stride]
                    + yVals[(i * size) + j + stride]) * .25f);
        }
    }
    /*
     Computes a center-point (a *-point below) by averaging the the four corner points (X):
     X   0   X
    
     0   *   0
     
     X   0   X
     */

    float avgSquare(int i, int j) {
        //System.out.println("square i: " + i + " j: " + j + " stride: " + stride);
        return (float) ((yVals[((i - stride) * size) + j - stride]
                + yVals[((i - stride) * size) + j + stride]
                + yVals[((i + stride) * size) + j - stride]
                + yVals[((i + stride) * size) + j + stride]) * .25f);

    }
    // A simple Laplacian smoothing algorithm that uses a weighted average of neighboring points.
    // Lambda (here (smooth/10f)) decides how heavy the weighting for the average of the nearby points is.
    void smoothing() {
        for (int i = 1; i < size - 1; i++) {
            for (int j = 1; j < size - 1; j++) {
                yVals[i * size + j] = yVals[i * size + j] + (smooth / 10f) * (((yVals[(i + 1) * size + j] + yVals[(i - 1) * size + j] + yVals[(i) * size + j - 1] + yVals[(i) * size + j + 1]) / 4.f) - yVals[i * size + j]);
            }
        }
    }
}
